package com.imooc.diners.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.StrUtil;
import com.imooc.commons.constant.ApiConstant;
import com.imooc.commons.constant.PointTypesConstant;
import com.imooc.commons.exception.ParameterException;
import com.imooc.commons.model.domain.ResultInfo;
import com.imooc.commons.model.vo.SignInDinerInfo;
import com.imooc.commons.utils.AssertUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;

/**
 * 用户签到Service
 *
 * @author gcq
 * @Create 2022-11-26
 */
@Service
public class SignService {

    @Value("${service.name.ms-oauth-server}")
    private String oauthServerName;

    @Value("${service.name.ms-points-server}")
    private String pointsServerName;

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 用户对应月份首次签到
     *
     * @param accessToken
     * @param date
     * @return
     */
    public long firstSign(String accessToken, String date){
        // 获取登录用户信息

        // 获取月份

        // 构建key

        // bitpos user:sign:202211 1 首次出现位置
        return 0;
    }

    // 完成用户每日登录统计

    /**
     * 获取当月签到情况
     *
     * @param accessToken
     * @param dateStr
     * @return
     */
    public Map<String, Boolean> getSignInfo(String accessToken, String dateStr){
        // 获取当前登录用户信息
        SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);

        // 获取日期
        Date date = getDate(dateStr);

        // 构建 key
        String signKey = buildSignKey(dinerInfo.getId(), date);

        // 构建一个自动排序的 Map
        Map<String, Boolean> signInfo = new TreeMap<>();

        // 获取某月的总天数 考虑闰年
        int dayOfMonth = DateUtil.lengthOfMonth(DateUtil.month(date) + 1,
                DateUtil.isLeapYear(DateUtil.dayOfYear(date)));

        // bitfield user:sign:5:202211 u30 8
        BitFieldSubCommands bitField = BitFieldSubCommands
                .create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);
        List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitField);
        if(list == null || list.isEmpty()){
            return signInfo;
        }
        long v = list.get(0) == null ? 0 : list.get(0);
        // 从低位到高位进行遍历，为 0 表示未签到，为 1 表示已签到
        for(int i = dayOfMonth; i > 0; i--) {
            /**
             * 签到   yyyy-MM-dd true
             * 未签到 yyyy-MM-dd false
             */
            LocalDateTime localDateTime = LocalDateTimeUtil.of(date).withDayOfMonth(dayOfMonth);
            // 左移然后右移，不等于则未签到，等于则已签到
            boolean flag = v >> 1 << 1 != v;
            signInfo.put(DateUtil.format(localDateTime, "yyyy-MM-dd"), flag);
            // 往右移下一位
            v >>= 1;
        }
        return signInfo;
    }

    /**
     * 获取用户签到次数
     *
     * @param accessToken
     * @param dateStr
     * @return
     */
    public long getSignCount(String accessToken, String dateStr) {
        // 获取用户信息
        SignInDinerInfo signInDinerInfo = loadSignInDinerInfo(accessToken);

        // 获取日期
        Date date = getDate(dateStr);

        // 构建key
        String signKey = buildSignKey(signInDinerInfo.getId(), date);

        // BITCOUNT user:sign:202211
        return (long) redisTemplate.execute((RedisCallback<Long>) con ->
                con.bitCount(signKey.getBytes()));
    }

    /**
     * 用户签到
     *
     *
     *
     * @param accessToken
     * @param dateStr
     * @return
     */
    public int doSing(String accessToken, String dateStr) {
        // 获取登录用户信息
        SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
        // 获取日期
        Date date = getDate(dateStr);

        // 获取日期对应的天数，多少号，从 0 开始 减一与 redis 中 bitmaps 数据结构对应 ,作为偏移量
        int offset = DateUtil.dayOfMonth(date) - 1;

        // 构建 key user:sign:6:yyyyMMdd
        String signKey = buildSignKey(dinerInfo.getId(), date);

        // 查看是否已签到
        // gitbit key 0 获取0号下标的数据
        Boolean isSigned = redisTemplate.opsForValue().getBit(signKey, offset);
        AssertUtil.isTrue(isSigned, "当前日期已完成签到，无需在签");

        // 签到
        // setbit 0 1
        redisTemplate.opsForValue().setBit(signKey, offset, true);

        // 统计连续签到天数
        int count = getContinuouSignCount(dinerInfo.getId(), date);
        // 签到获取积分,并返回
        int points = addPoints(count, dinerInfo.getId());
        return points;
    }

    /**
     * 统计连续签到次数
     *
     * 比如一个月 那么 就是31个 0/1的数据 00000000000000000000001111100000
     * 通过比较计算出当前用户本月连续签到多少天,也就是中间1连续出现的数量
     *
     *
     * @param dinerId 用户Id
     * @param date 日期
     * @return 连续签到天数
     */
    private int getContinuouSignCount(Integer dinerId, Date date) {
        // 获取日期对应天数，多少号，假设是31,来作为偏移量
        int dayOfMonth = DateUtil.dayOfMonth(date);
        String key = buildSignKey(dinerId, date);
        // 0 开始统计31天
        // user:sign:5:202211 u31 0
        // u31 只获取31个
        BitFieldSubCommands bitField = BitFieldSubCommands
                .create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);
        // 获取值就是 00000000000000000000001111100000 31位/本月的天数量
        List<Long> list = redisTemplate.opsForValue().bitField(key, bitField);
        if(list == null || list.isEmpty()){
            return 0;
        }
        int signCount = 0;
        /**
         * 如何统计用户连续签到天数
         * 思路：
         * 统计出1连续出现的次数，则就是连续签到天数
         * 如何统计？
         * 位移：
         * 二进制中 位移 就是把最低位丢弃
         * 右移则移除最右边值，改成0
         * 左移则移除最左边，改为0
         *
         * 高位                         低位
         * 1111111100010000000000000110000  31号  2,139,619,376 十进制数据
         * 右移以后
         * 011111110001000000000000011000  30号    1,065,877,552  十进制数据 【如何进行比较？如何知道签到未签到 如何解决：位移】
         * 然后再左移
         * 1111111100010000000000000110000
         * 结论:【最后面一位】右移再左移如果等于自己，则表示未签到
         *
         * 1111111100010000000000000110001
         * 右移以后
         * 0111111110001000000000000011000
         * 然后再左移
         * 1111111100010000000000000110000
         * 结论:右移再左移如果不等于自己，则表示已签到
         *
         * 循环丢弃，一直判断，是不是能获取到最终连续签到次数
         *
         */
        long v = list.get(0) == null ? 0 : list.get(0);
        // 假如 dayOfMonth = 31 大于0 每次减1 一直到处理所有
        // i表示位移操作次数
        for(int i = dayOfMonth; i > 0; i--) {
            // 左移在右移，如果等于自己说明最低位是0，表示未签到
            if(v >> 1 << 1 == v) {
                // 低位 0 且非当天说明连续签到中断了
                // i == dayOfMonth 等于则表示就是今天
                // i != dayOfMonth 不等于则表示不是今天 不是今天就break，出现了0，说白就是断了，
                if(i != dayOfMonth) break;
            } else {
                // 否则就是签到了
                signCount++;
            }
            // 右移一位并重新赋值，相当于把最低位丢弃一位
            // 31号、30号、29号、以此类推
            // v = v >> 1
            v >>= 1;
        }
        return signCount;
    }

    /**
     * 构建签到 Key - user:sign:dinerId:yyyyMM
     * 转义符 href=https://wenku.baidu.com/view/9bfec02c5b1b6bd97f192279168884868762b86b.html?_wkts_=1669461478017&bdQuery=string.format+%25%E8%BD%AC%E4%B9%89
     *
     * @param dinerId 当前用户Id
     * @param date 时间
     * @return key
     */
    private String buildSignKey(Integer dinerId, Date date) {
        // %d 数字 %s 时间
        return String.format("user:sign:%d:%s", dinerId,
                DateUtil.format(date, "yyyyMM"));
    }

    /**
     * 获取时间
     * 未传入则是当前时间
     *
     * @param dateStr 日期字符串 格式 yyyy-MM-dd
     * @return 时间
     */
    private Date getDate(String dateStr) {
        if (StrUtil.isBlank(dateStr)) {
            return new Date();
        }
        try {
            return DateUtil.parseDate(dateStr);
        } catch (Exception e) {
            // 抛出异常 日期不正确
            throw new ParameterException("请传入yyyy-MM-dd的日期格式");
        }
    }

    /**
     * 获取登录用户信息
     *
     * @param accessToken 访问token
     * @return 登录用户信息
     */
    private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
        AssertUtil.mustLogin(accessToken);

        String url = oauthServerName + "/user/me?access_token={accessToken}";
        ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            throw new ParameterException(resultInfo.getMessage());
        }
        SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(), new SignInDinerInfo(), false);
        if (dinerInfo == null) {
            throw new ParameterException(ApiConstant.NO_LOGIN_CODE, ApiConstant.NO_LOGIN_MESSAGE);
        }
        return dinerInfo;
    }

    /**
     * 添加用户积分
     *
     * @param count
     * @param signInDinerId
     * @return
     */
    private int addPoints(int count, Integer signInDinerId){
        // 签到一天送10积分 连续签到2天送20积分 签到三天送30积分 4天以上送50积分
        int points = 10;
        if(count == 2){
            points = 20;
        } else if(count == 3){
            points = 30;
        } else if(count >= 4){
            points = 50;
        }
        // 调用积分接口添加积分
        // 构建请求头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        // 构建请求体（请求参数）
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add("dinerId", signInDinerId);
        body.add("points", points);
        body.add("types", PointTypesConstant.sign.getType());
        HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
        // 发送请求
        ResponseEntity<ResultInfo> result = restTemplate.postForEntity(pointsServerName,
                entity, ResultInfo.class);
        AssertUtil.isTrue(result.getStatusCode() != HttpStatus.OK, "登录失败！");
        ResultInfo resultInfo = result.getBody();
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            // 失败了, 事物要进行回滚
            throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
        }
        return points;
    }
}